package com.io.typescript.codegen;

import com.io.typescript.codegen.utils.EnumUtil;
import io.swagger.codegen.v3.*;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.apache.commons.lang3.StringUtils;

import java.util.*;

import static io.swagger.codegen.v3.generators.handlebars.ExtensionHelper.getBooleanValue;

/**
 * 2022/8/11 17:35
 *
 * @author LiuWeiHao
 */
public class TypescriptGenerator extends AbstractTypeScriptClientCodegen {
    public static String DEFAULT_API_PACKAGE = "webapis/v/apis";
    public static String DEFAULT_MODEL_PACKAGE = "webapis/v/models";
    protected String npmRepository = null;
    public static String PREFIX = "JERRY";
    private String tsModelPackage = "";
    private String tsApiPackage = "";
    private List<String> responseModelList = new ArrayList<>();
    private List<String> logList = new ArrayList<>();

    public TypescriptGenerator() {
        super();
        importMapping.clear();
        outputFolder = "generated-code/typescript";
        templateDir = "typescript";
        responseModelList.clear();
    }

    @Override
    public String getIgnoreFilePathOverride() {
        Utils.deleteAllFile(getOutputDir(), true);
        return super.getIgnoreFilePathOverride();
    }

    /**
     * Configures the type of generator.
     *
     * @return the CodegenType for this generator
     * @see io.swagger.codegen.CodegenType
     */
    public CodegenType getTag() {
        return CodegenType.CLIENT;
    }

    /**
     * Configures a friendly name for the generator.  This will be used by the generator
     * to select the library with the -l flag.
     *
     * @return the friendly name for the generator
     */
    public String getName() {
        return "typescript";
    }

    /**
     * Returns human-friendly help for the generator.  Provide the consumer with help
     * tips, parameters here
     *
     * @return A string value for the help message
     */
    public String getHelp() {
        return "Generates a typescript client library.";
    }


    public String getNpmRepository() {
        return npmRepository;
    }

    public void setNpmRepository(String npmRepository) {
        this.npmRepository = npmRepository;
    }

    private static String getRelativeToRoot(String path) {
        StringBuilder sb = new StringBuilder();
        int slashCount = path.split("/").length;
        if (slashCount == 0) {
            sb.append("./");
        } else {
            for (int i = 0; i < slashCount; ++i) {
                sb.append("../");
            }
        }
        return sb.toString();
    }

    @Override
    public void processOpts() {
        super.processOpts();
    }

    private void loadProcessOpts() {
        if (StringUtils.isBlank(modelPackage)) {
            modelPackage = DEFAULT_MODEL_PACKAGE;
        }
        if (StringUtils.isBlank(apiPackage)) {
            apiPackage = DEFAULT_API_PACKAGE;
        }
        tsModelPackage = modelPackage.replaceAll("\\.", "/");
        tsApiPackage = apiPackage.replaceAll("\\.", "/");

        String modelRelativeToRoot = getRelativeToRoot(tsModelPackage);
        String apiRelativeToRoot = getRelativeToRoot(tsApiPackage);

        additionalProperties.put("tsModelPackage", tsModelPackage);
        additionalProperties.put("tsApiPackage", tsApiPackage);
        additionalProperties.put("apiRelativeToRoot", apiRelativeToRoot);
        additionalProperties.put("modelRelativeToRoot", modelRelativeToRoot);

        supportingFiles.add(new SupportingFile("index.mustache", "", "index.ts"));
        supportingFiles.add(new SupportingFile("baseApi.mustache", "", "base.ts"));
        supportingFiles.add(new SupportingFile("api.mustache", "", "api.ts"));
        supportingFiles.add(new SupportingFile("configuration.mustache", "", "configuration.ts"));
        supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
        supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
        supportingFiles.add(new SupportingFile("npmignore", "", ".npmignore"));

        modelTemplateFiles.put("model.mustache", ".ts");
        apiTemplateFiles.put("apiInner.mustache", ".ts");
        supportingFiles.add(new SupportingFile("modelIndex.mustache", tsModelPackage, "index.ts"));
        supportingFiles.add(new SupportingFile("apiIndex.mustache", tsApiPackage, "index.ts"));
        supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
        supportingFiles.add(new SupportingFile("package.mustache", "", "package.json"));
        supportingFiles.add(new SupportingFile("tsconfig.mustache", "", "tsconfig.json"));
    }

    @Override
    public void preprocessOpenAPI(OpenAPI openAPI) {
        super.preprocessOpenAPI(openAPI);
        Info info = openAPI.getInfo();
        String version = "v";
        if (info != null) {
            version = info.getVersion();
            switch (version) {
                case "1.0":
                    version = "v1";
                    break;
                case "2.0":
                    version = "v2";
                    break;
                case "3.0":
                    version = "v3";
                    break;
                default:
                    break;
            }
        }
        System.out.println("--------------version---" + version);
        DEFAULT_API_PACKAGE = "webapis/" + version + "/apis";
        DEFAULT_MODEL_PACKAGE = "webapis/" + version + "/models";
        this.processOpts();
        this.loadProcessOpts();
    }

    @Override
    public void processOpenAPI(OpenAPI openAPI) {
        super.processOpenAPI(openAPI);
        logList.addAll(0, responseModelList);
        Utils.write(logList);
        Utils.createNewFile(getOutputDir());
    }

    @Override
    public Map<String, Object> postProcessOperations(Map<String, Object> operations) {
        boolean hasImports = operations.get("hasImport") != null && Boolean.parseBoolean(operations.get("hasImport").toString());
        if (hasImports) {
            List<Map<String, String>> imports = (List<Map<String, String>>) operations.get("imports");

            for (Map<String, String> importMap : imports) {
                final String importValue = importMap.get("import");
                if (StringUtils.isNotBlank(importValue) && importValue.contains(".")) {
                    int index = importValue.indexOf(".");
                    importMap.put("import", importValue.substring(index + 1));
                }
            }
        }
        return operations;
    }

    @Override
    public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
        objs = super.postProcessOperationsWithModels(objs, allModels);
        Map<String, Object> vals = (Map<String, Object>) objs.getOrDefault("operations", new HashMap<>());
        List<CodegenOperation> operations = (List<CodegenOperation>) vals.getOrDefault("operation", new ArrayList<>());
        for (CodegenOperation operation : operations) {
            if (operation.path != null && operation.path.startsWith("/")) {
//                String version = String.valueOf(additionalProperties().get("appVersion"));
                if (operation.path.startsWith("/dev/")) {
                    operation.path = operation.path.substring("/dev/".length());
                } else {
                    operation.path = operation.path.substring(1);
                }
            }
            //
//            String[] split = operation.path.split("/");
//            operation.baseName = split[split.length - 2].toLowerCase(Locale.ROOT);
//            operation.nickname = split[split.length - 1].toLowerCase(Locale.ROOT);
//            logList.add(operation.baseName);
            if (operation.nickname != null) {
                if (operation.operationIdCamelCase.startsWith(operation.baseName)) {
                    operation.nickname = operation.operationIdCamelCase.substring(operation.baseName.length());
                    operation.nickname = toLowerCamelCase(operation.nickname);
                } else {
                    operation.nickname = toLowerCamelCase(operation.nickname);
                }
                //方法名关键字处理
                operation.nickname = nickNameFormat(operation.nickname);
            }
            if (operation.returnType != null) {
                if (operation.returnType.equals("boolean")) {
                    operation.returnType = "Boolean";
                }
                if (operation.returnType.equals("string")) {
                    operation.returnType = "String";
                }
            }

            List<CodegenResponse> responses = operation.responses;
            for (CodegenResponse codegenResponse : responses) {
                responseModelList.add(codegenResponse.dataType);
            }
        }
        //处理model
//        for (Object _mo : allModels) {
//            Map<String, Object> mo = (Map<String, Object>) _mo;
//            CodegenModel cm = (CodegenModel) mo.get("model");
////            if(cm.classname.equals("AuditGetOutput")){
////                System.out.println("--------------"+mo);
////            }
//            for (CodegenProperty codegenProperty : cm.vars) {
//                this.postProcessModelProperty(cm, codegenProperty);
//            }
//        }
        operations.stream().filter(op -> getBooleanValue(op, CodegenConstants.HAS_CONSUMES_EXT_NAME)).filter(op -> op.consumes.stream().anyMatch(opc -> opc.values().stream().anyMatch("multipart/form-data"::equals))).forEach(op -> op.vendorExtensions.putIfAbsent("multipartFormData", true));
        return objs;
    }

    @Override
    public Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
        Map<String, Object> result = super.postProcessAllModels(objs);
        for (Map.Entry<String, Object> entry : result.entrySet()) {
            Map<String, Object> inner = (Map<String, Object>) entry.getValue();
            List<Map<String, Object>> models = (List<Map<String, Object>>) inner.get("models");
            for (Map<String, Object> model : models) {
                CodegenModel codegenModel = (CodegenModel) model.get("model");
                //todo: model.put("hasAllOf", codegenModel.allOf.size() > 0);
                //todo: model.put("hasOneOf", codegenModel.oneOf.size() > 0);
            }
        }
        return result;
    }

    @Override
    public Map<String, Object> postProcessModels(Map<String, Object> objs) {
        List<Object> models = (List<Object>) postProcessModelsEnum(objs).get("models");
        String selfClassName = "";
        for (Object _mo : models) {
            Map<String, Object> mo = (Map<String, Object>) _mo;
            CodegenModel cm = (CodegenModel) mo.get("model");
            // Deduce the model file name in kebab case
            cm.classFilename = cm.classname.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(Locale.ROOT);
            if (cm.imports.contains(cm.classname)) {
                selfClassName = cm.classname;
            }
//            System.out.println(cm.classname + "------------" + cm.imports);
            //processed enum names
            cm.imports = new TreeSet(cm.imports);
            // name enum with model name, e.g. StatusEnum => PetStatusEnum
            for (CodegenProperty var : cm.vars) {
                if (getBooleanValue(var, CodegenConstants.IS_ENUM_EXT_NAME)) {
                    var.datatypeWithEnum = var.datatypeWithEnum.replace(var.enumName, cm.classname + var.enumName);
                    var.enumName = var.enumName.replace(var.enumName, cm.classname + var.enumName);
                }
            }
            if (cm.parent != null) {
                for (CodegenProperty var : cm.allVars) {
                    if (getBooleanValue(var, CodegenConstants.IS_ENUM_EXT_NAME)) {
                        var.datatypeWithEnum = var.datatypeWithEnum.replace(var.enumName, cm.classname + var.enumName);
                        var.enumName = var.enumName.replace(var.enumName, cm.classname + var.enumName);
                    }
                }
            }
        }

        // Apply the model file name to the imports as well
        for (Map<String, String> m : (List<Map<String, String>>) objs.get("imports")) {
            String javaImport = m.get("import").substring(modelPackage.length() + 1);
            String tsImport = tsModelPackage + "/" + javaImport;
            if (!javaImport.equals(selfClassName)) {
                m.put("tsImport", tsImport);
                m.put("class", javaImport);
                m.put("filename", javaImport.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(Locale.ROOT));
            }
        }


        return objs;
    }

    @Override
    public Map<String, Object> postProcessModelsEnum(Map<String, Object> objs) {
        List<Object> models = (List<Object>) objs.get("models");
        for (Object _mo : models) {
            Map<String, Object> mo = (Map<String, Object>) _mo;
            CodegenModel cm = (CodegenModel) mo.get("model");
            boolean succes = buildEnumFromVendorExtension(cm) ||
                    buildEnumFromValues(cm);
            for (CodegenProperty var : cm.vars) {
                updateCodegenPropertyEnum(var);
            }
        }
        return objs;
    }

    public boolean buildEnumFromValues(CodegenModel cm) {
        if (!cm.getIsEnum() || cm.allowableValues == null) {
            return false;
        }
        Map<String, Object> allowableValues = cm.allowableValues;
        List<Object> values = (List<Object>) allowableValues.get("values");
        List<Map<String, String>> enumVars = new ArrayList<Map<String, String>>();
        String commonPrefix = findCommonPrefixOfVars(values);
        int truncateIdx = commonPrefix.length();
        if(EnumUtil.descHasAllValue(cm.description,values)){
            Map<String, String> descMap = EnumUtil.parseEnum(cm.description);
            for(Object value:values){
                Map<String, String> enumVar = new HashMap<>();
                enumVar.put("name", toEnumVarName(descMap.get(value.toString().trim()),cm.dataType));
                enumVar.put("value", toEnumValue(value.toString(), cm.dataType));
                enumVars.add(enumVar);
            }
        }else {
            for (Object value : values) {
                Map<String, String> enumVar = new HashMap<>();
                String enumName;
                if (truncateIdx == 0) {
                    enumName = value.toString();
                } else {
                    enumName = value.toString().substring(truncateIdx);
                    if ("".equals(enumName)) {
                        enumName = value.toString();
                    }
                }
                enumVar.put("name", toEnumVarName(enumName, cm.dataType));
                enumVar.put("value", toEnumValue(value.toString(), cm.dataType));
                enumVars.add(enumVar);
            }
        }
        cm.allowableValues.put("enumVars", enumVars);
        return true;
    }

    private boolean buildEnumFromVendorExtension(CodegenModel cm) {
        if (!cm.getIsEnum() || cm.allowableValues == null ||
                !cm.vendorExtensions.containsKey("x-enum-values")) {
            return false;
        }
        Object extension = cm.vendorExtensions.get("x-enum-values");
        List<Map<String, Object>> values =
                (List<Map<String, Object>>) extension;
        List<Map<String, String>> enumVars =
                new ArrayList<Map<String, String>>();
        for (Map<String, Object> value : values) {
            Map<String, String> enumVar = new HashMap<String, String>();
            String name = camelize((String) value.get("identifier"), true);
            if (isReservedWord(name)) {
                name = escapeReservedWord(name);
            }
            enumVar.put("name", name);
            enumVar.put("value", toEnumValue(
                    value.get("numericValue").toString(), cm.dataType));
            if (value.containsKey("description")) {
                enumVar.put("description", value.get("description").toString());
            }
            enumVars.add(enumVar);
        }
        cm.allowableValues.put("enumVars", enumVars);
        return true;
    }
    //    @Override
//    public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
//        super.postProcessModelProperty(model, property);
//        if (responseModelList.contains(model.classname)) {
//            property.required = true;
//        } else {
//            property.nullable = false;
//        }
//    }
//    @Override
//    public CodegenModel fromModel(String name, Schema schema, Map<String, Schema> allDefinitions) {
//        final CodegenModel codegenModel = super.fromModel(name, schema, allDefinitions);
//        return codegenModel;
//    }

//    @Override
//    public Map<String, Object> postProcessSupportingFileData(Map<String, Object> bundle) {
////        List<Object> models = (List<Object>) bundle.get("models");
////        for (Object _mo : models) {
////            Map<String, Object> mo = (Map<String, Object>) _mo;
////            CodegenModel cm = (CodegenModel) mo.get("model");
////            if (responseModelList.contains(cm.classname)) {
////                cm.emptyVars = true;
////                logList.add(cm.classname + "-------"+cm.emptyVars);
////            } else {
////                cm.emptyVars = false;
////            }
////            mo.put("model",cm);
////
////        }
////        bundle.put("models", models);
//        return bundle;
//    }

    /**
     * Overriding toRegularExpression() to avoid escapeText() being called,
     * as it would return a broken regular expression if any escaped character / metacharacter were present.
     */
    @Override
    public String toRegularExpression(String pattern) {
        return addRegularExpressionDelimiter(pattern);
    }


    @Override
    public String toApiFilename(String name) {
        String fileName = super.toApiFilename(name).replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(Locale.ROOT);
        if (fileName.endsWith("-api")) {
            fileName = fileName.substring(0, fileName.lastIndexOf("-api"));
        }
        return fileName;
    }

    @Override
    protected String getTemplateDir() {
        return templateDir;
    }

    @Override
    public String getDefaultTemplateDir() {
        return templateDir;
    }

    @Override
    public String toOperationId(String operationId) {
        return super.toOperationId(operationId);
    }

    private String toLowerCamelCase(String value) {
        return value.substring(0, 1).toLowerCase(Locale.ROOT) + value.substring(1);
    }


    @Override
    public String toModelName(String name) {
//        if (name.contains(".")) {
//            String[] split = name.split("\\.");
//            name = split[split.length - 1];
//        }
        if (name.contains("[")) {
            String name1 = name.substring(0, name.indexOf("["));
            if (name1.contains(".")) {
                name1 = name1.substring(name1.lastIndexOf(".") + 1);
            }
            name1 = sanitizeName(name1);
            name1 = name1.replaceAll("1", "");
            String name2 = name.substring(name.indexOf("["));
            if (name2.contains(".")) {
                name2 = name2.substring(name2.lastIndexOf(".") + 1);
            }
            name2 = sanitizeName(name2);
            name = name1 + "-" + name2;

        } else {
            if (name.contains(".")) {
                name = name.substring(name.lastIndexOf(".") + 1);
            }

        }
        return super.toModelName(name);
    }

    @Override
    public String toModelFilename(String name) {
        return super.toModelFilename(name).replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(Locale.ROOT);
    }

    private String nickNameFormat(String nickName) {
        if (nickName.equals("delete")) {
            return "del";
        }
        if (nickName.equals("return")) {
            return "returns";
        }
        if (nickName.equals("throw")) {
            return "throws";
        }
        return nickName;
    }
}